﻿using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Timers;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Media3D;
using InSimDotNet;
using InSimDotNet.Packets;
using LFSRecord2.Events;
using LFSRecord2.Misc;
using LFSRecord2.Model.ProjectStorage;
using LFSRecord2.Model.Replay;
using LFSRecord2.Model.Undo;
using LFSRecord2.Model.UserActions;
using LFSRecord2.Structures;
using LFSRecord2.View;
using LFSRecord2.ViewModel;
using Microsoft.Practices.Composite.Presentation.Events;
using LFSRecord2.Model.Sound;

namespace LFSRecord2.Model
{
    public static class LfsRecordController
    {
        /// <summary>
        /// Alias for AppDomain.CurrentDomain.BaseDirectory basically
        /// </summary>
        public static string StartupDirectory { get; set; }

        /// <summary>
        /// A global key input listener, used for hotkeys when recording from LFS in fullscreen
        /// </summary>
        private static KeyboardListener _kbListener = null;

        /// <summary>
        /// Indicates wether our program has focus (within windows)
        /// </summary>
        private static bool _hasFocus = true;

        /// <summary>
        /// The project contains all our project data (layers / properties / keyframes)
        /// </summary>
        public static LfsRecordProject project = new LfsRecordProject();

        /// <summary>
        /// Keeps track of the state of LFS (sta flags, mostly, but also things like current track)
        /// </summary>
        public static LfsState LfsState { get; set; }

        /// <summary>
        /// Insim socket handler (spark)
        /// </summary>
        public static InSim insim { get; set; }

        /// <summary>
        /// Timer object used for timeline playback, BUT NOT recording.
        /// </summary>
        private static System.Timers.Timer _playbackTimer = new System.Timers.Timer();

        /// <summary>
        /// Stopwatch helper to keep accurate time while playing
        /// </summary>
        private static Stopwatch _playbackStopWatch = new Stopwatch();

        /// <summary>
        /// Indicates the state of playback
        /// </summary>
        public static PlaybackStates PlaybackState { get; set; }

        public static SoundMixer Mixer { get; private set; }

        /// <summary>
        /// If your camera goes off the LFS track path, weird position artifacts can occur.
        /// These can often be avoided (for recordings, not for playback) by setting SafeOffPathRecording. (100 is a good number)
        /// This introduces a delay (in ms) allowing LFS to properly redraw the screen, before we take the screenshot.
        /// Must make this into a project setting still... for now it's just used as a sort of safety delay. 
        /// Not all people have fast computers, so this is just there to give those a little bit more breathing space while recording.
        /// </summary>
        public static int SafeRecordingDelay = 20;

        /// <summary>
        /// Indicates the current frame on the timeline
        /// </summary>
        public static int FrameIndex { get; set; }

        /// <summary>
        /// Indicates the current timeline time in milliseconds (Used for playing back only. The more important variable is FrameIndex which is used all over the place.)
        /// </summary>
        private static long _timelineIndex = 0;

        /// <summary>
        /// Indicates the time in ms between two frames (so this changes when FPS is changed)
        /// </summary>
        private static double _frameTime;

        /// <summary>
        /// Not really used atm. Was used for an option to sync LFS Record's currently active camera with LFS's camera.
        /// </summary>
        private static byte _mustSyncNextCPP = 0;

        /// <summary>
        /// Whenever we encounter an unsolvable error in our LFSRecordController methods, we report the problem to this callback, 
        /// which goes to MainWindowViewModel, which can display a messagebox on the UI thread.
        /// More times than not, methods in LFSRecordController run on another thread instead, so can't really invoke the UI thread from here. 
        /// Hence we need a callback (or something at least) to reach an object created by the UI thread.
        /// </summary>
        private static ShowGenericErrorCallback _genericErrorCallback;

        private static ReplayErrorCallback _replayErrorCallback;

        /// <summary>
        /// Our undo / redo manager object.
        /// </summary>
        private static UndoManager _undo = new UndoManager();

        // NASTY nasty fix for itemscontrol sub-object default scale
        private static double _viewportScale = 1;
        public static double ViewportScale
        {
            get { return _viewportScale; }
            set
            {
                _viewportScale = value;
                //setViewportMatrix();
            }
        }

        private static ViewportViews _viewportView = ViewportViews.Top;
        public static ViewportViews ViewportView
        {
            get { return _viewportView; }
            set
            {
                _viewportView = value;
                setViewportMatrix();
            }
        }

        public static Matrix3D ViewTransformMatrix = new Matrix3D();
        private static void setViewportMatrix()
        {
            ViewTransformMatrix = new Matrix3D();
            switch (_viewportView)
            {
                case ViewportViews.Top:
                    // Nothing to do
                    break;
                case ViewportViews.Left:
                    // Rotate 90 degrees over X and 90 degrees over Z
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), 90));
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(0, 1, 0), 90));
                    break;
                case ViewportViews.Front:
                    // Rotate 90 degrees over X
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), 90));
                    break;
                case ViewportViews.Bottom:
                    // Rotate 180 degrees over X
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), 180));
                    break;
                case ViewportViews.Right:
                    // Rotate 90 degrees over X and -90 degrees over Z
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), 90));
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(0, 1, 0), -90));
                    break;
                case ViewportViews.Back:
                    // Rotate 180 degrees over Y and -90 degrees over X
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(0, 1, 0), 180));
                    ViewTransformMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), -90));
                    break;
            }
            //ViewTransformMatrix.Scale(new Vector3D(1 / ViewportScale, 1 / ViewportScale, 1 / ViewportScale));
        }

        /// <summary>
        /// Called when starting the program, to initialize this static class and setup default project values.
        /// </summary>
        /// <param name="replayErrorCallback">The callback delegate used to report errors on when there is an error during MCI recording. Crappy and will change.</param>
        /// <param name="genericErrorCallback">The callback delegate used to report generic errors to, so the error message can be presented to the user in a messagebox (on the UI thread)</param>
        public static void SetupElements(ReplayErrorCallback replayErrorCallback, ShowGenericErrorCallback genericErrorCallback)
        {
            _replayErrorCallback = replayErrorCallback;
            _genericErrorCallback = genericErrorCallback;

            StartupDirectory = AppDomain.CurrentDomain.BaseDirectory;

            BitmapCache.initialize();

            DefaultComposition("New Project");

            LfsState = new LfsState();
            Mixer = new SoundMixer(project);
        }

        public static void SetupListeners()
        {
            ServicesFactory.EventService.GetEvent<WorldCamChangedEvent>().Subscribe(OnCameraChanged);
            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Subscribe(OnFrameIndexChanged, ThreadOption.UIThread);
            ServicesFactory.EventService.GetEvent<ViewportScaleChangedEvent>().Subscribe(ViewportScaleChangedHandler);
            ServicesFactory.EventService.GetEvent<ViewportViewChangedEvent>().Subscribe(ViewportViewChangedHandler);
            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Subscribe(CompLayerPropertyChangedHandler);

            _playbackTimer.Elapsed += new ElapsedEventHandler(OnNextTimerTick);

            Application.Current.Activated += new EventHandler(Current_Activated);
            Application.Current.Deactivated += new EventHandler(Current_Deactivated);
        }

        public static void Dispose()
        {
            StopPlayback();

            ServicesFactory.EventService.GetEvent<WorldCamChangedEvent>().Unsubscribe(OnCameraChanged);
            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Unsubscribe(OnFrameIndexChanged);
            ServicesFactory.EventService.GetEvent<ViewportScaleChangedEvent>().Unsubscribe(ViewportScaleChangedHandler);
            ServicesFactory.EventService.GetEvent<ViewportViewChangedEvent>().Unsubscribe(ViewportViewChangedHandler);
            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Unsubscribe(CompLayerPropertyChangedHandler);

            Mixer.Dispose();
        }

        [DebuggerStepThrough]
        static void Current_Deactivated(object sender, EventArgs e)
        {
            _hasFocus = false;
        }

        [DebuggerStepThrough]
        static void Current_Activated(object sender, EventArgs e)
        {
            _hasFocus = true;
        }

        #region ProjectNewLoadSave
        /// <summary>
        /// Clear the whole current project at once. Called before either loading a project or starting a new one. This action is irreversable (no undo).
        /// </summary>
        public static void DisposeProject()
        {
            StopPlayback();
            FrameIndex = 0;
            LfsRecordController.project.deleteAll();
        }

        /// <summary>
        /// Creates a new default composition, including a default camera layer, while keeping generic project settings in tact
        /// </summary>
        /// <param name="compName">The name of the project</param>
        public static void DefaultComposition(string compName)
        {
            DefaultComposition(compName, true);
        }
        /// <summary>
        /// Creates a new default composition, while keeping generic project settings in tact
        /// </summary>
        /// <param name="compName">The name of the project</param>
        /// <param name="defaultLayers">Indicates wether to populate the new project with the default look camera or leave everything blank.</param>
        public static void DefaultComposition(string compName, bool defaultLayers)
        {
            // Set new project
            project.ProjectName = compName;
            if (defaultLayers)
            {
                ((CompLayerLookCamera)project.AddLookCamera()).IsActiveCamera = true;
            }

            project.SetDefaultValues();

            // Timer
            _playbackTimer.Stop();
            SetTimerInterval();
            PlaybackState = PlaybackStates.Stopped;

            // Reset other values
            _timelineIndex = 0;
            FrameIndex = 0;

            // Reset undo manager
            _undo.Reset();
        }

        /// <summary>
        /// Load a project from file.
        /// </summary>
        /// <param name="projectFilePath">The full file path (so path + filename.ext). Normally as provided by LoadFileDialog.</param>
        /// <returns>Returns true if all went well, false if there was a problem loading the file.</returns>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public static void LoadProject(string projectFilePath)
        {
            Debug.WriteLine("Loading project : '" + projectFilePath + "'");

            try
            {
                using (FileStream fs = File.OpenRead(projectFilePath))
                using (BinaryReader bfs = new BinaryReader(fs, Encoding.ASCII))
                {
                    LoadProject(projectFilePath, fs, bfs);
                }
            }
            catch
            {
                DisposeProject();
                DefaultComposition("New Project", false);
                throw;
            }
        }

        private static void LoadProject(string projectFilePath, FileStream fs, BinaryReader bfs)
        {
            // Verify that we're (probably) going to load an actual LFSRecord project (and not an old version either)
            if (fs.Length < 257 || FileHelper.CharArrayToString(bfs.ReadChars(10)) != "LFSRecord")
            {
                throw new ProjectLoadException("Error loading project :\n\nFile does not appear to be a LFSRecord project file");
            }

            int saveVersion = bfs.ReadUInt16();
            switch (saveVersion)
            {
                case 2:
                    // Now we know we're (probably) going to load a new project, reset it first
                    DisposeProject();
                    // Set project to default (without default layer(s))
                    DefaultComposition(FileHelper.CharArrayToString(bfs.ReadChars(22), true), false);
                    try
                    {
                        ProjectStorageV2.Load(project, fs, bfs);
                    }
                    catch (ProjectLoadException ex)
                    {
                        throw ex;
                    }
                    break;
                case 3:
                    DisposeProject();
                    DefaultComposition(FileHelper.CharArrayToString(bfs.ReadChars(22), true), false);
                    try
                    {
                        ProjectStorageV3.Load(project, fs, bfs);
                    }
                    catch (ProjectLoadException ex)
                    {
                        throw ex;
                    }
                    break;
                default:
                    throw new ProjectLoadException("Error loading project :\n\nOutdated or invalid file version");
            }

            // Store file path so we can do straight saves the next time
            project.ProjectFilePath = projectFilePath;

            _undo.Reset();
            //Undo.AddUndoLevel(project, new UndoActionArgs());

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = null, NewFrameIndex = 0, UpdateCurrentValues = true });
        }

        /// <summary>
        /// Save the current project.
        /// </summary>
        /// <param name="projectFilePath">The full filepath (so path + filename.ext), normally as returned by the SaveFileDialog</param>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        public static void SaveProject(string projectFilePath)
        {
            Debug.WriteLine("Saving project : '" + projectFilePath + "'");

            using (FileStream fs = File.OpenWrite(projectFilePath))
            using (BinaryWriter bfs = new BinaryWriter(fs, Encoding.ASCII))
            {
                SaveProject(projectFilePath, fs, bfs);
            }
        }

        private static void SaveProject(string projectFilePath, FileStream fs, BinaryWriter bfs)
        {
            // Store file path so we can do straight saves the next time
            // C:\Users\Victor\Documents\Visual Studio 2010\Projects\LFSRecord\New Project.lfsrec
            string splitChar = "\\";
            string[] exp = projectFilePath.Split((char)splitChar[0]);
            int splitPos = exp[exp.Length - 1].LastIndexOf(".", StringComparison.OrdinalIgnoreCase);
            if (project.ProjectName.Length == 0 || project.ProjectName == "New Project")
                project.ProjectName = Path.GetFileNameWithoutExtension(projectFilePath);
            project.ProjectFilePath = projectFilePath;

            Debug.WriteLine("ProjectName after save : " + project.ProjectName);

            // Write save data
            ProjectStorageV3.Save(project, fs, bfs);

            project.Edited = false;
        }

        public static bool ExportLFSRecordCustomPath(string filePath)
        {
            try
            {
                PathExport.ExportLFSRecordCustomPath(project, filePath);
            }
            catch (Exception ex)
            {
                //MessageBox.Show(Application.Current.MainWindow, "Error exporting custom LFSRecord path :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
                _genericErrorCallback("File Error", "Error exporting custom LFSRecord path :\n\n" + ex.Message);
                return false;
            }
            //catch (IOException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting custom LFSRecord path :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}
            //catch (NotSupportedException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting custom LFSRecord path :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}
            //catch (UnauthorizedAccessException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting custom LFSRecord path :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}

            return true;
        }

        public static bool ExportAFXPath(ExportAEDialogDataTemplate exportSettings, string filePath)
        {
            try
            {
                PathExport.ExportAFXPath(project, exportSettings, filePath);
            }
            catch (Exception ex)
            {
                //MessageBox.Show(Application.Current.MainWindow, "Error exporting AFX JavaScript file :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
                _genericErrorCallback("File Error", "Error exporting AFX JavaScript file :\n\n" + ex.Message);
                return false;
            }
            //catch (IOException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting AFX JavaScript file :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}
            //catch (NotSupportedException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting AFX JavaScript file :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}
            //catch (UnauthorizedAccessException ex)
            //{
            //    MessageBox.Show(Application.Current.MainWindow, "Error exporting AFX JavaScript file :\n\n" + ex.Message, "File Error", MessageBoxButton.OK, MessageBoxImage.Error);
            //    return false;
            //}

            return true;
        }

        #endregion ProjectNewLoadSave

        #region UndoRedo
        public static void RecordUserAction(UserActionBase action)
        {
            RecordUserAction(action, false);
        }
        public static void RecordUserAction(UserActionBase action, bool prepend)
        {
            if (prepend)
                _undo.PrependUserAction(action);
            else
                _undo.AddUserAction(action);
        }

        /// <summary>
        /// Commits all the recorded user actions into a new undo level
        /// </summary>
        public static void CreateUndoLevel()
        {
            _undo.CommitUserActions();
            project.Edited = true;
        }

        public static bool CanUndo()
        {
            return _undo.CanUndo();
        }

        public static void ApplyUndo()
        {
            if (!_undo.DoUndo(project))
                return;

            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = null, PropertyType = "Undo", UpdateCurrentValues = true });
        }

        public static bool CanRedo()
        {
            return _undo.CanRedo();
        }

        public static void ApplyRedo()
        {
            if (!_undo.DoRedo(project))
                return;

            ServicesFactory.EventService.GetEvent<CompLayerPropertyChangedEvent>().Publish(new CompLayerPropertyChangedEventArgs() { Sender = null, PropertyType = "Redo", UpdateCurrentValues = true });
        }
        #endregion UndoRedo

        #region InSim
        /// <summary>
        /// Resets our insim object. It creates a new InSim object and detaches (if needed) and re-attaches the listeners.
        /// </summary>
        public static void ResetInSim()
        {
            if (insim != null)
            {
                insim.Initialized -= new EventHandler<InitializeEventArgs>(OnInSimConnect);
                insim.Disconnected -= new EventHandler<DisconnectedEventArgs>(OnInSimDisconnect);
                insim.InSimError -= new EventHandler<InSimErrorEventArgs>(OnInSimError);

                RemoveDefaultInSimListeners(true);

                //insim.PacketReceived -= new EventHandler<PacketEventArgs>(insim_PacketReceived);
            }

            insim = new InSim();
            insim.Initialized += new EventHandler<InitializeEventArgs>(OnInSimConnect);
            insim.Disconnected += new EventHandler<DisconnectedEventArgs>(OnInSimDisconnect);
            insim.InSimError += new EventHandler<InSimErrorEventArgs>(OnInSimError);

            AddDefaultInSimListeners(true);

            //insim.PacketReceived += new EventHandler<PacketEventArgs>(insim_PacketReceived);
        }

        public static void RemoveDefaultInSimListeners(bool removeSta)
        {
            if (removeSta)
                insim.Unbind<IS_STA>(OnInSimStateChanged);
            insim.Unbind<IS_ISM>(LfsState.InSimMulti);
            insim.Unbind<IS_VER>(LfsState.InSimVer);
            insim.Unbind<IS_CPP>(OnLFSCameraChanged);
            insim.Unbind<IS_SSH>(OnScreenshotConfirm);
            insim.Unbind<IS_RIP>(OnReplayConfirm);
        }

        public static void AddDefaultInSimListeners(bool addSta)
        {
            if (addSta)
                insim.Unbind<IS_STA>(OnInSimStateChanged);
            insim.Bind<IS_STA>(OnInSimStateChanged);
            insim.Bind<IS_ISM>(LfsState.InSimMulti);
            insim.Bind<IS_VER>(LfsState.InSimVer);
            insim.Bind<IS_CPP>(OnLFSCameraChanged);
            insim.Bind<IS_SSH>(OnScreenshotConfirm);
            insim.Bind<IS_RIP>(OnReplayConfirm);
        }

        /// <summary>
        /// Connect to LFS. This uses the connection settings from the project.
        /// </summary>
        public static void ConnectInSim()
        {
            ResetInSim();

            insim.Initialize(new InSimSettings
            {
                Host = project.InSimIP,
                Port = project.InSimPort,
                IName = "LFSRecord",
                Admin = project.InSimAdminPassword,
                Flags = InSimFlags.ISF_MCI,
            });
        }

        /// <summary>
        /// Close an open insim connection.
        /// </summary>
        public static void DisconnectInSim()
        {
            insim.Disconnect();
            LfsState.Flags = 0;
        }

        /// <summary>
        /// Handler for when insim connects
        /// </summary>
        /// <param name="sender">The object that raised the event</param>
        /// <param name="e">The connection event information</param>
        public static void OnInSimConnect(Object sender, InitializeEventArgs e)
        {
            FreezeLFS(false);
            insim.Send(TinyType.TINY_SST, 1);
            insim.Send(TinyType.TINY_ISM, 1);
            ServicesFactory.EventService.GetEvent<InSimConnectionChangedEvent>().Publish(true);
        }

        /// <summary>
        /// Handler for when insim disconnects
        /// </summary>
        /// <param name="sender">The object that raised the event</param>
        /// <param name="e">The disconnect event information</param>
        public static void OnInSimDisconnect(Object sender, DisconnectedEventArgs e)
        {
            ServicesFactory.EventService.GetEvent<InSimConnectionChangedEvent>().Publish(false);
            Debug.WriteLine("DISCONNECTED INSIM!!!!!!!_________------------ " + e.Reason);
        }

        /// <summary>
        /// Handler for when an insim error occurs. 
        /// Though this sometimes seems to handle errors in methods outside the insim object, but invoked by the insim object as well. 
        /// For example packet handlers that reside outside of the insim object. 
        /// 
        /// Alex Note: Yes, it will handle errors outside the InSim object. The packets are handled on a seperate thread which has 
        /// its own stack, so any exceptions which occur in the packet handlers won't get caught by the main program thread. To get around 
        /// this InSim.NET catches the exceptions itself and raises events for them, otherwise there is no way to know that an 
        /// exception occured and .NET will just appear to fail silently.
        /// </summary>
        /// <param name="sender">The object that raised the event</param>
        /// <param name="e">The error event information</param>
        public static void OnInSimError(Object sender, InSimErrorEventArgs e)
        {
            Debug.WriteLine("InSim error : " + e.Exception);

            Logger.LogError(e.Exception.ToString());

            ServicesFactory.EventService.GetEvent<InSimErrorRaisedEventArgs>().Publish(e.Exception);
        }

        /// <summary>
        /// Handler for the IS_STA packet
        /// </summary>
        /// <param name="sta">The IS_STA packet</param>
        public static void OnInSimStateChanged(InSim insim, IS_STA sta)
        {
            LfsState.StateInfo(insim, sta);
            if (!LfsState.HasSynced && LfsState.Flags.HasFlag(StateFlags.ISS_SHIFTU))
            {
                insim.Send(TinyType.TINY_SCP, 1);
                LfsState.HasSynced = true;
            }
        }

        /// <summary>
        /// Handler for the IS_CPP packet.
        /// (not really used atm though)
        /// </summary>
        /// <param name="cpp">The IS_CPP packet</param>
        public static void OnLFSCameraChanged(InSim insim, IS_CPP cpp)
        {
            LfsCamera lfsCam = new LfsCamera(project.GetActiveCameraType());
            lfsCam.PosX = cpp.Pos.X / 65536.0f;
            lfsCam.PosY = cpp.Pos.Y / 65536.0f;
            lfsCam.PosZ = cpp.Pos.Z / 65536.0f;
            lfsCam.Heading = cpp.H / LfsHelper.LfsDegMult;
            lfsCam.Pitch = cpp.P / LfsHelper.LfsDegMult;
            lfsCam.Roll = cpp.R / LfsHelper.LfsDegMult;
            lfsCam.FOV = cpp.FOV;
            Debug.WriteLine("Current X : " + lfsCam.PosX + " Y : " + lfsCam.PosY + " Z : " + lfsCam.PosZ);

            if (_mustSyncNextCPP > 0 && _mustSyncNextCPP == cpp.ReqI)
            {
                //ServicesFactory.EventService.GetEvent<SyncCamFromLFSEvent>().Publish(lfsCam);
                _mustSyncNextCPP = 0;
            }

            //ServicesFactory.EventService.GetEvent<LFSCameraChangedEvent>().Publish(lfsCam);
        }

        /// <summary>
        /// Handler for the IS_SSH packet.
        /// This tells us when LFS is done taking a screenshot, so that we can move on to the next frame (when recording).
        /// </summary>
        /// <param name="ssh">The IS_SSH packet</param>
        private static void OnScreenshotConfirm(InSim insim, IS_SSH ssh)
        {
            //Debug.WriteLine("IS_SSH Error result : " + ssh.Error);

            // Whenever we have dumped the screen in LFS, we get this packet.
            // We must decide if this was 'just' a screenshot, or if we're in a recording loop
            if ((PlaybackState != PlaybackStates.Recording && PlaybackState != PlaybackStates.RecordingReplay))
            {
                return;
            }
            else if (ssh.Error > 0)
            {
                // Log error
                Logger.LogError("Screenshot saving error (" + ssh.Error.ToString() + ")");
            }

            ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs()
                {
                    Sender = null,
                    NewFrameIndex = FrameIndex + 1,
                    UpdateCurrentValues = true
                });
        }

        public static void GetCppFromLfs()
        {
            insim.Send(TinyType.TINY_SCP, 1);
        }

        /// <summary>
        /// Handler for the IS_RIP packet.
        /// This informs us about the result of our replay-seek activities and let's us know when LFS is ready again, after seeking inside a replay.
        /// </summary>
        /// <param name="rip">The IS_RIP packet</param>
        private static void OnReplayConfirm(InSim insim, IS_RIP rip)
        {
            _isSeekingReplay = false;

            //if (RipHandleState == ReplayReplyHandleStates.Ignore)
            //    return;

            switch (rip.Error)
            {
                case ReplayError.RIP_CORRUPTED:
                case ReplayError.RIP_DEDICATED:
                case ReplayError.RIP_DEST_OOB:
                case ReplayError.RIP_NOT_FOUND:
                case ReplayError.RIP_NOT_REPLAY:
                case ReplayError.RIP_OOS:
                case ReplayError.RIP_UNKNOWN:
                case ReplayError.RIP_UNLOADABLE:
                case ReplayError.RIP_USER:
                case ReplayError.RIP_WRONG_MODE:

                    // Skip error if playback stopped, to prevent errors being spammed.
                    if (PlaybackState == PlaybackStates.Stopped)
                    {
                        Debug.WriteLine("Replay error '{0}' skipped as playback stopped", rip.Error);
                        return;
                    }

                    Debug.WriteLine("Replay Error : " + rip.Error);
                    // Replay error under generic circumstances
                    PauseLFS(true);
                    SetShiftU(true);
                    if (PlaybackState != PlaybackStates.Stopped)
                        StopPlayback();

                    _replayErrorCallback(rip.Error);
                    System.Threading.Thread.Sleep(100);
                    return;
                default:
                    // If a new replay is loaded
                    if (LfsState.CurrentReplayName != rip.RName)
                    {
                        LfsState.CurrentReplayName = rip.RName;

                        // quick shiftu fix (always going to shift-u mode now, but maybe that's good actually. Not sure yet if i can do camera controlling in non-shiftu modes)
                        SetShiftU(true);
                        System.Threading.Thread.Sleep(200);

                        // Here we must put LFS back into the proper view state
                        LfsCamera cam = project.GetActiveCameraAtIndex(FrameIndex);
                        OnCameraChanged(new WorldCamChangedEventArgs() { Sender = null, Cam = cam });

                        // for some reason we need to sleep in order for the continued replaying to go well
                        // LFS needs some time to handle the replay change I guess
                        System.Threading.Thread.Sleep(500);

                        Debug.WriteLine("Loaded new replay : " + rip.RName);
                    }
                    break;
            }

            if (rip.Paused)
            {
                LfsState.Flags |= StateFlags.ISS_PAUSED;

                if (PlaybackState == PlaybackStates.PlayingReplay || PlaybackState == PlaybackStates.RecordingReplay)
                {
                    ContinuePlaybackReplay();
                }
            }
        }

        /// <summary>
        /// Generic insim packet handler. I only used this for debugging and a quick view on what's going on.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public static void insim_PacketReceived(object sender, PacketEventArgs e)
        {
            switch (e.Packet.Type)
            {
                case PacketType.ISP_MCI:
                    //Debug.WriteLine("Received packet of type : " + e.Packet.Type);
                    break;

                default:
                    break;
            }
        }

        #endregion InSim

        #region PlaybackRecording
        public static void SetTimerInterval()
        {
            _frameTime = 1000.0f / project.FPS;
            _playbackTimer.Interval = 1;
            //PlaybackTimer.Interval = _frameTime;
        }

        public static void StartPlayback()
        {
            if (PlaybackState == PlaybackStates.Playing || PlaybackState == PlaybackStates.PlayingReplay)
                return;

            // Adjust process priority for accuracy
            SetThreadPriority(true);

            _timelineIndex = (long)(FrameIndex * _frameTime);

            // Check if we are replaying a replay
            if (project.HasReplay() && insim != null && insim.IsConnected)
            {
                PlaybackState = PlaybackStates.PlayingReplay;

                // Seek in replay
                SetReplayPosition(true);
            }
            else
            {
                try
                {
                    Mixer.StartPlayback(FrameIndex / (double)project.FPS);
                    PlaybackState = PlaybackStates.Playing;
                    SetTimerInterval();
                    _playbackTimer.Start();
                    _playbackStopWatch.Reset();
                    _playbackStopWatch.Start();
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("start-waveout exception : " + ex.Message);
                    StopPlayback();
                    _genericErrorCallback("Audio Error", "Audio engine could not be started (" + ex.Message + ")");
                }
            }
        }

        static bool _isSeekingReplay = false;
        public static void SetReplayPosition(bool paused)
        {
            if (insim == null || !insim.IsConnected)
                return;

            ReplayRecordInfo rInfo = project.GetCurrentReplayInfo();

            //Debug.WriteLine("setting replay at frame {0} - time {1}", FrameIndex, rInfo.ReplayOffset);
            // Do we have to load a new replay?
            if (rInfo.ReplayName.Length > 4 && LfsState.CurrentReplayName != Path.GetFileNameWithoutExtension(rInfo.ReplayName))
            {
                Debug.WriteLine("Setting replay position to : {0} / {1}", rInfo.ReplayName, (uint)Math.Round(rInfo.ReplayOffset * 100));

                paused = true;
                PrepareForDifferentReplay(rInfo.ReplayName);
            }

            SetReplay(rInfo.ReplayName, paused, TimeSpan.FromSeconds(rInfo.ReplayOffset));
        }

        private static void SetReplay(string replayName, bool paused, TimeSpan cTime)
        {
            // Get extension and determine replay type (spr/mpr)
            string ext = Path.GetExtension(replayName).ToUpperInvariant();
            replayName = Path.GetFileNameWithoutExtension(replayName);

            // Send packet
            insim.Send(new IS_RIP
            {
                ReqI = 1,
                MPR = (ext == ".MPR") ? ReplayMode.Mpr : ReplayMode.Spr,
                Paused = paused,
                CTime = cTime,
                RName = replayName
            });

            _isSeekingReplay = true;
        }

        public static void PrepareForDifferentReplay(string newReplayName)
        {
            _playbackTimer.Stop();

            if (LfsState.CurrentReplayName == Path.GetFileNameWithoutExtension(newReplayName))
                return;

            // Make sure we're not in shift-u mode.
            SetShiftU(false);

            // Do we have to get out of regular LFS mode, so we can load a replay?
            if (!LfsState.Flags.HasFlag(StateFlags.ISS_REPLAY))
                SetLFSMainMenu();

            FreezeLFS(false);

            System.Threading.Thread.Sleep(400);
        }

        public static void ContinuePlaybackReplay()
        {
            if (insim == null || !insim.IsConnected)
            {
                StopPlayback();
                return;
            }

            PauseLFS(false);
            FreezeLFS(true);

            if (PlaybackState == PlaybackStates.PlayingReplay)
            {
                // Start playback timer
                SetTimerInterval();
                _timelineIndex = (long)(FrameIndex * _frameTime);
                Mixer.StartPlayback(FrameIndex / (double)project.FPS);
                _playbackTimer.Start();

                _playbackStopWatch.Reset();
                _playbackStopWatch.Start();
            }
            else if (PlaybackState == PlaybackStates.RecordingReplay)
            {
                InSimScreenCapture();
            }
        }

        public static void StopPlayback()
        {
            // Regular thread priority
            if (PlaybackState != PlaybackStates.Stopped)
            {
                SetThreadPriority(false);
            }

            // Cleanup hotkey listener
            if (_kbListener != null)
            {
                _kbListener.KeyDown -= HotKeyDownListener;
                _kbListener.Dispose();
                _kbListener = null;
            }

            // Show 2d display again
            if (project.RecordHide2D)
                Set2DDisplay(true);

            // Go back to windowed mode
            if (project.RecordFullScreenOverride)
                SetFullScreen(false);

            // Post replay-playback action
            if (PlaybackState == PlaybackStates.PlayingReplay || PlaybackState == PlaybackStates.RecordingReplay)
            {
                SetReplayPosition(true);
                FreezeLFS(false);
            }

            // Stop playback or reset
            if (PlaybackState != PlaybackStates.Stopped)
            {
                PlaybackState = PlaybackStates.Stopped;
                _playbackTimer.Stop();
                
                _playbackStopWatch.Stop();
                _playbackStopWatch.Reset();

                Mixer.StopPlayback();
            }
            else
            {
                ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = null, NewFrameIndex = 0, UpdateCurrentValues = true, UpdateReplayPosition = true });
            }
        }

        public static void StartRecord()
        {
            if (insim == null || !insim.IsConnected)
            {
                //MessageBox.Show(Application.Current.MainWindow, "You must be connected to an LFS client in order to record.\n\nRecording is done by screendumping each individual frame inside LFS itself. These screendumps are stored in your LFS/data/shots folder.", "Record error", MessageBoxButton.OK, MessageBoxImage.Error);
                _genericErrorCallback("Record error", "You must be connected to an LFS client in order to record.\n\nRecording is done by screendumping each individual frame inside LFS itself. These screendumps are stored in your LFS/data/shots folder.");
                return;
            }

            if (PlaybackState == PlaybackStates.Playing || PlaybackState == PlaybackStates.PlayingReplay)
                StopPlayback();
            else if (PlaybackState != PlaybackStates.Stopped)
                return;

            // Set hotkey listener
            _kbListener = new KeyboardListener();
            _kbListener.KeyDown += HotKeyDownListener;

            if (project.RecordHide2D)
                Set2DDisplay(false);

            if (project.RecordFullScreenOverride)
            {
                SetFullScreen(true);
                Thread.Sleep(2000);     // we need to sleep quite a bit to let LFS redraw after resize
            }

            // Record mode with replay or not
            if (project.HasReplay())
            {
                PlaybackState = PlaybackStates.RecordingReplay;
                SetReplayPosition(true);
            }
            else
            {
                PlaybackState = PlaybackStates.Recording;
                InSimScreenCapture();
            }
        }

        public static void InSimScreenCapture()
        {
            if ((PlaybackState != PlaybackStates.Recording && PlaybackState != PlaybackStates.RecordingReplay) || insim == null || !insim.IsConnected)
                return;

            if (SafeRecordingDelay > 0)
                System.Threading.Thread.Sleep(SafeRecordingDelay);

            insim.Send(new IS_SSH
            {
                ReqI = 1,
                BMP = project.ProjectName + "_" + FrameIndex.ToString("D8", CultureInfo.InvariantCulture)
            });

            // Reached end of project?
            if (FrameIndex / (double)project.FPS >= project.ProjectLength)
            {
                PlaybackState = PlaybackStates.Stopped;
                return;
            }
        }

        public static void OnNextTimerTick(object sender, ElapsedEventArgs e)
        {
            if (FrameIndex / (double)project.FPS >= project.ProjectLength)
            {
                StopPlayback();
                return;
            }

            int prevFrameIndex = FrameIndex;
            FrameIndex = (int)((_timelineIndex + _playbackStopWatch.ElapsedMilliseconds) / _frameTime);

            if (FrameIndex > prevFrameIndex)
            {
                ServicesFactory.EventService.GetEvent<FrameIndexChangedEvent>().Publish(new FrameIndexChangedEventArgs() { Sender = null, NewFrameIndex = FrameIndex, UpdateCurrentValues = true });
                //Debug.WriteLine("timertick on frame {0}, timelineindex : {1}", FrameIndex, TimelineIndex);
            }
        }

        #endregion PlaybackRecording

        #region LFSStateSetters
        public static void PauseLFS(bool pause)
        {
            if (insim == null || !insim.IsConnected)
                return;

            if (pause)
            {
                if (!LfsState.Flags.HasFlag(StateFlags.ISS_PAUSED))
                {
                    Debug.WriteLine("PAUSING LFS");
                    insim.Send("/press p");
                }
            }
            else
            {
                if (LfsState.Flags.HasFlag(StateFlags.ISS_PAUSED))
                {
                    Debug.WriteLine("UN-PAUSING LFS");
                    insim.Send("/press p");
                }
            }
        }

        public static void FreezeLFS(bool freeze)
        {
            if (insim == null || !insim.IsConnected)
                return;

            Debug.WriteLine(freeze ? "FREEZING LFS" : "UN-FREEZING LFS");

            insim.Send(SmallType.SMALL_TMS, freeze ? 1 : 0);
        }

        public static void SetShiftU(bool shiftU)
        {
            if (insim == null || !insim.IsConnected)
                return;

            LfsState.ShiftUInternal = shiftU;

            if (shiftU)
            {
                if (!LfsState.Flags.HasFlag(StateFlags.ISS_SHIFTU))
                {
                    Debug.WriteLine("entering shift-u mode");
                    insim.Send("/shift u");
                }
            }
            else
            {
                if (LfsState.Flags.HasFlag(StateFlags.ISS_SHIFTU))
                {
                    Debug.WriteLine("going out of shift-u mode");
                    insim.Send(new IS_SCH { CharB = (char)27 });
                }
            }
        }

        public static void SetFullScreen(bool fullScreen)
        {
            if (insim == null || !insim.IsConnected)
                return;

            insim.Send(new IS_MOD
            {
                Width = fullScreen ? project.RecordingWidth : 0,
                Height = fullScreen ? project.RecordingHeight : 0
            });
        }

        public static void Set2DDisplay(bool display)
        {
            if (insim == null || !insim.IsConnected)
                return;

            insim.Send(new IS_SFP
            {

                Flag = StateFlags.ISS_SHOW_2D,
                OffOn = display,
            });
        }

        public static void SetLFSMainMenu()
        {
            if (insim == null || !insim.IsConnected)
                return;

            insim.Send("/entry");
        }
        #endregion LFSStateSetters

        #region EventHandlers
        /// <summary>
        /// Either Timeline, View or Controller publish this one, so that we know when to update LFS
        /// </summary>
        /// <param name="e">The information about the new camera state</param>
        public static void OnCameraChanged(WorldCamChangedEventArgs e)
        {
            //if (e.Sender == null)
            //    return;

            //Debug.WriteLine("Sending camera packet to lfs on frame " + FrameIndex);
            // Send IS_CPP to update LFS's camera position
            if (insim != null && insim.IsConnected && e.Cam != null && (LfsState.Flags & StateFlags.ISS_FRONT_END) == 0)
            {
                //Debug.WriteLine("Sending CPP to lfs");
                e.Cam.Heading = e.Cam.Heading % 360;
                e.Cam.Pitch = e.Cam.Pitch % 360;
                e.Cam.Roll = e.Cam.Roll % 360;

                IS_CPP cpp = new IS_CPP();
                cpp.Flags = (LfsState.ShiftUInternal) ? StateFlags.ISS_SHIFTU : 0;
                cpp.Pos = new Vec((int)(e.Cam.PosX * 65536), (int)(-e.Cam.PosY * 65536), (int)(e.Cam.PosZ * 65536));
                cpp.H = (ushort)(-e.Cam.Heading * LfsHelper.LfsDegMult);
                cpp.P = (ushort)(e.Cam.Pitch * LfsHelper.LfsDegMult);
                cpp.R = (ushort)(e.Cam.Roll * LfsHelper.LfsDegMult);
                cpp.FOV = (float)e.Cam.FOV;
                
                insim.Send(cpp);
            }
        }

        /// <summary>
        /// Handler for the FrameIndexChangedEvent, called every time FrameIndex must change.
        /// </summary>
        /// <param name="e">The information about who triggered the frame change and what the new FrameIndex should be.</param>
        public static void OnFrameIndexChanged(FrameIndexChangedEventArgs e)
        {
            int oldFrameIndex = FrameIndex;
            FrameIndex = e.NewFrameIndex;

            // If user clicked timeline ruler, or any other object that changes the frame index
            if (e.Sender != null)
            {
                if (PlaybackState == PlaybackStates.Recording || PlaybackState == PlaybackStates.RecordingReplay)
                {
                    StopPlayback();
                }
                else if (PlaybackState == PlaybackStates.Playing)
                {
                    StopPlayback();
                    _timelineIndex = (long)(FrameIndex * _frameTime);
                    StartPlayback();
                    return;
                }
            }

            if (PlaybackState == PlaybackStates.Stopped)
            {
                // If we have a replay...
                if (e.UpdateReplayPosition && insim != null && insim.IsConnected && project.HasReplay())
                {
                    SetReplayPosition(true);
                }
            }

            // Must we update all the layers' property "currentvalue" values?
            if (e.UpdateCurrentValues)
                project.SetCurrentValues(FrameIndex);
            project.SetKeyWalkerToggles();

            //Debug.WriteLine("Controller OnFrameIndexChanged " + e.NewFrameIndex);
            LfsCamera cam = project.GetActiveCameraAtIndex(FrameIndex);
            if (cam == null)
            {
                //Debug.WriteLine("Cam is null after timelineindex changed handler (we have no (active) camera layer)");
                StopPlayback();
                return;
            }

            // Fire off a global event which will trigger methods that send the camera info to LFS and our 3D viewport.
            ServicesFactory.EventService.GetEvent<WorldCamChangedEvent>().Publish(new WorldCamChangedEventArgs() { Sender = null, Cam = cam });

            // Decide what to do when we're recording or playing back a replay
            if (PlaybackState == PlaybackStates.PlayingReplay)
            {
                // Regular replay seek, but only if LFS is not already (or still) seeking.
                if (!_isSeekingReplay)
                    SetReplayPosition(false);
            }
            else if (PlaybackState == PlaybackStates.RecordingReplay)
            {
                // Move the replay position to the current FrameIndex and pause
                SetReplayPosition(true);
            }
            else if (PlaybackState == PlaybackStates.Recording)
            {
                InSimScreenCapture();
            }
        }

        /// <summary>
        /// Handler for global CompLayerPropertyChangedEvent which is published every time something changes about a layer or property (or keyframe), 
        /// so that we can update those things related to the change that do not automatically update.
        /// </summary>
        /// <param name="e">Information about the event</param>
        public static void CompLayerPropertyChangedHandler(CompLayerPropertyChangedEventArgs e)
        {
            if (e.UpdateCurrentValues)
                project.SetCurrentValues(FrameIndex);

            LfsCamera cam = project.GetActiveCameraAtIndex(FrameIndex);
            if (cam == null)
                return;

            ServicesFactory.EventService.GetEvent<WorldCamChangedEvent>().Publish(new WorldCamChangedEventArgs() { Sender = null, Cam = cam });
        }

        public static void ViewportScaleChangedHandler(double scale)
        {
            ViewportScale = scale;
        }

        public static void ViewportViewChangedHandler(ViewportViewChangedEventArgs e)
        {
            ViewportView = e.View;
        }

        /// <summary>
        /// Listens for keystrokes when we do not have focus (when we're in the background, for example while recording fullscreen)
        /// </summary>
        /// <param name="sender">The sender</param>
        /// <param name="e">Keyboard event args</param>
        private static void HotKeyDownListener(object sender, RawKeyEventArgs e)
        {
            //Debug.WriteLine("HotKey listener : " + ((int)e.Key) + " - vkcode : " + e.VKCode);

            // We only do this if we do not have focus
            if (_hasFocus)
                return;

            //Debug.WriteLine("HotKey listener : " + e.Key);
            if (e.VKCode == project.RecordStopHotkey || e.Key == Key.Escape)
                StopPlayback();
        }

        #endregion EventHandlers

        /// <summary>
        /// Set the current process's priority according to what we need.
        /// Normally we want regular priority and thread affinity, but while playing back, to get better timings, we need to stick our thread to one processor for less thread swapping overhead.
        /// </summary>
        /// <param name="prioritise">true if you want playback priority (high) or false for regular priority (normal).</param>
        public static void SetThreadPriority(bool prioritise)
        {
            //if (prioritise)
            //{
            //    if (Environment.ProcessorCount > 1)
            //        Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2);
            //    //Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
            //    //Thread.CurrentThread.Priority = ThreadPriority.Normal;
            //}
            //else
            //{
            //    if (Environment.ProcessorCount > 1)
            //        Process.GetCurrentProcess().ProcessorAffinity = (IntPtr)(Math.Pow(2, Environment.ProcessorCount) - 1);
            //    //Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
            //    //Thread.CurrentThread.Priority = ThreadPriority.Normal;
            //}
        }

        /// <summary>
        /// Factory method for ImportRpd object, populated from project data. Used as template object for ImportRpdFromLfsDialog.xaml
        /// </summary>
        /// <returns>ImportRpd object</returns>
        public static ImportRpdFromLfs CreateImportRpdFromLfs()
        {
            // Create and populate the object
            ImportRpdFromLfs importRpd = new ImportRpdFromLfs(insim);

            // Loop through available replays in project
            CompLayerPropertyString propRName;
            CompLayerPropertyDouble propRTime;
            CustomStringKeyFrame kfs;
            double projectStartTime, projectEndTime;
            foreach (CompLayerBase layer in project.Layers)
            {
                if (layer.LayerType == CompLayerTypes.ReplayControl)
                {
                    propRName = (CompLayerPropertyString)layer.Properties[(int)CompLayerReplayControl.PropertyTypes.ReplayName];
                    propRTime = (CompLayerPropertyDouble)layer.Properties[(int)CompLayerReplayControl.PropertyTypes.TimeOffset];
                    if (propRName.KeyFrames.Count == 0)
                    {
                        // There's only one replay in the project
                        projectStartTime = (double)propRTime.GetValueAtFrame(0);
                        projectEndTime = (double)propRTime.GetValueAtFrame((int)(project.ProjectLength * project.FPS));
                        if (projectStartTime > projectEndTime)
                        {
                            projectStartTime = projectEndTime;
                            projectEndTime = (double)propRTime.GetValueAtFrame(0);
                        }

                        importRpd.RInfo.Add(new ImportRpdSectionInfo()
                            {
                                ReplayName = propRName.CurrentValue,
                                ProjectStartTime = projectStartTime,
                                ProjectEndTime = projectEndTime,
                                RecordStartTime = projectStartTime,
                                RecordEndTime = projectEndTime
                            });
                    }
                    else
                    {
                        // There is more than 1 replay in the project
                        for (int a = 0; a < propRName.KeyFrames.Count; a++)
                        {
                            kfs = (CustomStringKeyFrame)propRName.KeyFrames[a];

                            projectStartTime = kfs.FrameNum / (double)project.FPS;
                            if (a + 1 < propRName.KeyFrames.Count)
                                projectEndTime = (propRName.KeyFrames[a + 1].FrameNum - 1) / (double)project.FPS;
                            else
                                projectEndTime = (int)(project.ProjectLength);
                            
                            if (projectStartTime > projectEndTime)
                            {
                                projectStartTime = projectEndTime;
                                projectEndTime = kfs.FrameNum / (double)project.FPS;
                            }

                            importRpd.RInfo.Add(new ImportRpdSectionInfo()
                                {
                                    ReplayName = kfs.Value,
                                    ProjectStartTime = projectStartTime,
                                    ProjectEndTime = projectEndTime,
                                    RecordStartTime = projectStartTime,
                                    RecordEndTime = projectEndTime
                                });
                        }
                    }
                    break;
                }
            }

            return importRpd;
        }
    }
}
